﻿namespace AY38910
{
    public class AyPortState
    {
        #region Fields

        private bool _dirOut = true;

        private byte _oldOutState;

        private byte _outState;

        #endregion

        #region Constructors and Destructors

        public AyPortState(byte value)
        {
            this._outState = value;
            this._oldOutState = value;
            this.InState = value;
        }

        #endregion

        #region Public Properties

        public bool DirOut
        {
            get
            {
                return this._dirOut;
            }
            set
            {
                this._dirOut = value;
            }
        }

        public byte InState { get; set; }

        public byte OldOutState
        {
            get
            {
                return this._oldOutState;
            }
        }

        public byte OutState
        {
            get
            {
                return this._outState;
            }
            set
            {
                this._oldOutState = this._outState;
                this._outState = value;
            }
        }

        #endregion
    }

    public delegate void AyUpdatePortDelegate(AY8910 sender, AyPortState state);

    public class AY8910
    {
        #region Constants

        private const int MAX_ENV_VOLTBL = 0x1F;

        private const int SMP_T_RELATION = 5;

        #endregion

        #region Fields

        public uint[] samples = null;

        private readonly byte[] ayRegMasks = new byte[] { 0xFF, 0x0F, 0xFF, 0x0F, 0xFF, 0x0F, 0x1F, 0xFF, 0x1F, 0x1F, 0x1F, 0xFF, 0xFF, 0x0F, 0xFF, 0xFF };

        private readonly uint[] YM_VolTableA = new uint[32];

        private readonly uint[] YM_VolTableB = new uint[32];

        private readonly uint[] ymVolTableC = new uint[32];

        private readonly ushort[] ymTab = new ushort[32]
                                               {
                                                   0x0000, 0x0000, 0x00F8, 0x01C2, 0x029E, 0x033A, 0x03F2, 0x04D7, 0x0610, 0x077F, 0x090A, 0x0A42, 0x0C3B, 0x0EC2, 0x1137, 0x13A7, 0x1750, 0x1BF9, 0x20DF,
                                                   0x2596, 0x2C9D, 0x3579, 0x3E55, 0x4768, 0x54FF, 0x6624, 0x773B, 0x883F, 0xA1DA, 0xC0FC, 0xE094, 0xFFFF
                                               };

        private readonly int _frameTactCount;

        private readonly AyPortState _ira = new AyPortState(0xFF);

        private readonly AyPortState _irb = new AyPortState(0xFF);

        private readonly int _sampleRate;

        private readonly uint[] mixerPreset = new uint[] { 90, 15, 60, 60, 15, 90 }; // ABC   values in 0...100

        private byte BendStatus;

        private int BendVolumeIndex;

        private byte ControlBend; //13

        private byte ControlChannels; //7

        private ushort CounterA;

        private ushort CounterB;

        private ushort CounterC;

        private byte CounterNoise;

        private ushort FreqA;

        private ushort FreqB;

        private ushort FreqBend; //11

        private ushort FreqC;

        private byte FreqNoise; //6

        private byte MixLineABC; //  0 or 1 per channel - mixed channel 

        private uint NoiseVal = 0xFFFF;

        private byte OutputABC;

        private byte OutputNoiseABC;

        private byte VolumeA; //8

        private byte VolumeB; //9

        private byte VolumeC; //10

        private uint _CounterBend;

        private byte _curReg;

        private int samplePos;

        #endregion

        #region Constructors and Destructors

        public AY8910(int sampleRate, int frameTactCount)
        {
            this._frameTactCount = frameTactCount;
            this._sampleRate = sampleRate;
            this.samples = new uint[this._sampleRate / 50];
            this.Volume = 9000;
        }

        #endregion

        #region Public Events

        public event AyUpdatePortDelegate UpdateIRA;

        public event AyUpdatePortDelegate UpdateIRB;

        #endregion

        #region Public Properties

        public byte ADDR_REG
        {
            get
            {
                return this._curReg;
            }
            set
            {
                this._curReg = value;
            }
        }

        public void SetDATA_REG(byte value)
        {
            if ((this._curReg > 7) && (this._curReg < 11))
            {
                if ((value & 0x10) != 0)
                {
                    value &= 0x10;
                }
                else
                {
                    value &= 0x0F;
                }
            }
            value &= this.ayRegMasks[this._curReg & 0x0F];
            switch (this._curReg)
            {
                case 0:
                    this.FreqA = (ushort)((this.FreqA & 0xFF00) | value);
                    break;
                case 1:
                    this.FreqA = (ushort)((this.FreqA & 0x00FF) | (value << 8));
                    break;
                case 2:
                    this.FreqB = (ushort)((this.FreqB & 0xFF00) | value);
                    break;
                case 3:
                    this.FreqB = (ushort)((this.FreqB & 0x00FF) | (value << 8));
                    break;
                case 4:
                    this.FreqC = (ushort)((this.FreqC & 0xFF00) | value);
                    break;
                case 5:
                    this.FreqC = (ushort)((this.FreqC & 0x00FF) | (value << 8));
                    break;
                case 6:
                    this.FreqNoise = value;
                    break;
                case 7:
                    this.ControlChannels = value;
                    break;
                case 8:
                    this.VolumeA = value;
                    break;
                case 9:
                    this.VolumeB = value;
                    break;
                case 10:
                    this.VolumeC = value;
                    break;
                case 11:
                    this.FreqBend = (ushort)((this.FreqBend & 0xFF00) | value);
                    break;
                case 12:
                    this.FreqBend = (ushort)((this.FreqBend & 0x00FF) | (value << 8));
                    break;
                case 13:
                    // ATT:
                    // 0 - begin down
                    // 1 - begin up
                    this._CounterBend = 0;
                    this.ControlBend = value;
                    if (this.FreqBend != 0)
                    {
                        if ((value & 0x04) != 0)
                        {
                            this.BendVolumeIndex = 0;
                            this.BendStatus = 1; // up
                        }
                        else
                        {
                            this.BendVolumeIndex = MAX_ENV_VOLTBL;
                            this.BendStatus = 2; // down
                        }
                    }
                    break;
                case 14:
                    this.OnUpdateIRA(value);
                    break;
                case 15:
                    this.OnUpdateIRB(value);
                    break;
            }
        }

        public byte GetDATA_REG()
        {
            switch (this._curReg)
            {
                case 0:
                    return (byte)(this.FreqA & 0xFF);
                case 1:
                    return (byte)(this.FreqA >> 8);
                case 2:
                    return (byte)(this.FreqB & 0xFF);
                case 3:
                    return (byte)(this.FreqB >> 8);
                case 4:
                    return (byte)(this.FreqC & 0xFF);
                case 5:
                    return (byte)(this.FreqC >> 8);
                case 6:
                    return this.FreqNoise;
                case 7:
                    return this.ControlChannels;
                case 8:
                    return this.VolumeA;
                case 9:
                    return this.VolumeB;
                case 10:
                    return this.VolumeC;
                case 11:
                    return (byte)(this.FreqBend & 0xFF);
                case 12:
                    return (byte)(this.FreqBend >> 8);
                case 13:
                    return this.ControlBend;
                case 14: // ay mouse
                    this.OnUpdateIRA(this._ira.OutState);
                    return this._ira.InState;
                case 15:
                    this.OnUpdateIRB(this._irb.OutState);
                    return this._irb.InState;
            }
            return 0;
        }

        public int Volume
        {
            set
            {
                int VolumeAY = value;
                ushort[] vol_table = this.ymTab;
                for (int i = 0; i < 32; i++)
                {
                    this.YM_VolTableA[i] = (uint)((vol_table[i] * VolumeAY / 65535 * this.mixerPreset[2 * 0] / 100) + ((vol_table[i] * VolumeAY / 65535 * this.mixerPreset[2 * 0 + 1] / 100) << 16));
                    this.YM_VolTableB[i] = (uint)((vol_table[i] * VolumeAY / 65535 * this.mixerPreset[2 * 1] / 100) + ((vol_table[i] * VolumeAY / 65535 * this.mixerPreset[2 * 1 + 1] / 100) << 16));
                    this.ymVolTableC[i] = (uint)((vol_table[i] * VolumeAY / 65535 * this.mixerPreset[2 * 2] / 100) + ((vol_table[i] * VolumeAY / 65535 * this.mixerPreset[2 * 2 + 1] / 100) << 16));
                }
            }
        }

        #endregion

        #region Public Methods and Operators

        public void Reset()
        {
            for (int i = 0; i < 16; i++)
            {
                this.ADDR_REG = (byte)i;
                this.SetDATA_REG(0);
            }
            this.process((this.samplePos * this._frameTactCount) / this.samples.Length);
        }

        public uint[] flush()
        {
            this.process(this._frameTactCount);
            this.samplePos = 0;
            return this.samples;
        }

        public void process(int tact)
        {
            if (tact > this._frameTactCount)
            {
                tact = this._frameTactCount;
            }
            this.GenSignalAY((this.samples.Length * tact) / this._frameTactCount);
        }

        #endregion

        #region Methods

        protected void OnUpdateIRA(byte outState)
        {
            this._ira.DirOut = (this.ControlChannels & 0x40) != 0;
            this._ira.OutState = outState;
            if (this._ira.DirOut)
            {
                this._ira.InState = outState;
            }
            else
            {
                this._ira.InState = 0x00;
            }
            if (this.UpdateIRA != null)
            {
                this.UpdateIRA(this, this._ira);
            }
        }

        protected void OnUpdateIRB(byte outState)
        {
            this._irb.DirOut = (this.ControlChannels & 0x80) != 0;
            this._irb.OutState = outState;
            if (this._irb.DirOut)
            {
                this._irb.InState = outState;
            }
            else
            {
                this._irb.InState = 0x00; // (see "TAIPAN" #A8CD)
            }
            if (this.UpdateIRB != null)
            {
                this.UpdateIRB(this, this._irb);
            }
        }

        private void ChangeEnvelope()
        {
            if (this.BendStatus == 0)
            {
                return;
            }
            // Êîððåêöèÿ àìïëèòóäû îãèáàþùåé:
            if (this.BendStatus == 1) // AmplUp
            {
                if (++this.BendVolumeIndex > MAX_ENV_VOLTBL) // DUoverflow
                {
                    switch (this.ControlBend & 0x0F)
                    {
                        case 0:
                            this.env_DD();
                            break; // not used
                        case 1:
                            this.env_DD();
                            break; // not used
                        case 2:
                            this.env_DD();
                            break; // not used
                        case 3:
                            this.env_DD();
                            break; // not used
                        case 4:
                            this.env_DD();
                            break;
                        case 5:
                            this.env_DD();
                            break;
                        case 6:
                            this.env_DD();
                            break;
                        case 7:
                            this.env_DD();
                            break;
                        case 8:
                            this.env_UD();
                            break; // not used
                        case 9:
                            this.env_DD();
                            break; // not used
                        case 10:
                            this.env_UD();
                            break;
                        case 11:
                            this.env_UU();
                            break; // not used
                        case 12:
                            this.env_DU();
                            break;
                        case 13:
                            this.env_UU();
                            break;
                        case 14:
                            this.env_UD();
                            break;
                        case 15:
                            this.env_DD();
                            break;
                    }
                }
            }
            else // AmplDown
            {
                if (--this.BendVolumeIndex < 0) // UDoverflow
                {
                    switch (this.ControlBend & 0x0F)
                    {
                        case 0:
                            this.env_DD();
                            break;
                        case 1:
                            this.env_DD();
                            break; //env_UU(); break;
                        case 2:
                            this.env_DD();
                            break;
                        case 3:
                            this.env_DD();
                            break;
                        case 4:
                            this.env_DD();
                            break; // not used
                        case 5:
                            this.env_DD();
                            break; // not used
                        case 6:
                            this.env_DD();
                            break; // not used
                        case 7:
                            this.env_DD();
                            break; // not used
                        case 8:
                            this.env_UD();
                            break;
                        case 9:
                            this.env_DD();
                            break;
                        case 10:
                            this.env_DU();
                            break;
                        case 11:
                            this.env_UU();
                            break;
                        case 12:
                            this.env_DU();
                            break; // not used
                        case 13:
                            this.env_UU();
                            break; // not used
                        case 14:
                            this.env_DU();
                            break;
                        case 15:
                            this.env_DD();
                            break; // not used
                    }
                }
            }
        }

        private void GenSignalAY(int toEndPtr)
        {
            byte MixResultA = 0; // result volume index for channel
            byte MixResultB = 0;
            byte MixResultC = 0;
            lock (this)
            {
                if (toEndPtr > 882)
                {
                    toEndPtr = 882;
                }
                // mixer
                for (; this.samplePos < toEndPtr; this.samplePos++)
                {
                    this.OutputNoiseABC &= (byte)(((this.ControlChannels & 0x38) >> 3) ^ 7);
                    this.MixLineABC = (byte)((this.OutputABC & (((this.ControlChannels & 0x07)) ^ 7)) ^ this.OutputNoiseABC);

                    if ((this.MixLineABC & 0x01) == 0)
                    {
                        MixResultA = (byte)((this.VolumeA & 0x1F) << 1);
                        if ((MixResultA & 0x20) != 0)
                        {
                            MixResultA = (byte)this.BendVolumeIndex;
                        }
                        else
                        {
                            MixResultA++;
                        }
                    }
                    else
                    {
                        MixResultA = 0;
                    }

                    if ((this.MixLineABC & 0x02) == 0)
                    {
                        MixResultB = (byte)((this.VolumeB & 0x1F) << 1);
                        if ((MixResultB & 0x20) != 0)
                        {
                            MixResultB = (byte)this.BendVolumeIndex;
                        }
                        else
                        {
                            MixResultB++;
                        }
                    }
                    else
                    {
                        MixResultB = 0;
                    }

                    if ((this.MixLineABC & 0x04) == 0)
                    {
                        MixResultC = (byte)((this.VolumeC & 0x1F) << 1);
                        if ((MixResultC & 0x20) != 0)
                        {
                            MixResultC = (byte)this.BendVolumeIndex;
                        }
                        else
                        {
                            MixResultC++;
                        }
                    }
                    else
                    {
                        MixResultC = 0;
                    }

                    this.samples[this.samplePos] = this.ymVolTableC[MixResultC] + this.YM_VolTableB[MixResultB] + this.YM_VolTableA[MixResultA];
                    // end of mixer

                    // ...counters...
                    for (int i = 0; i < SMP_T_RELATION; i++)
                    {
                        // noise counter...
                        if (++this.CounterNoise >= this.FreqNoise)
                        {
                            this.CounterNoise = 0;
                            this.NoiseVal = (((this.NoiseVal >> 16) ^ (this.NoiseVal >> 13)) & 1) ^ ((this.NoiseVal << 1) + 1);
                            this.OutputNoiseABC = (byte)((this.NoiseVal >> 16) & 1);
                            this.OutputNoiseABC |= (byte)((this.OutputNoiseABC << 1) | (this.OutputNoiseABC << 2));
                        }

                        // signals counters...
                        if (++this.CounterA >= this.FreqA)
                        {
                            this.CounterA = 0;
                            this.OutputABC ^= 1;
                        }
                        if (++this.CounterB >= this.FreqB)
                        {
                            this.CounterB = 0;
                            this.OutputABC ^= 2;
                        }
                        if (++this.CounterC >= this.FreqC)
                        {
                            this.CounterC = 0;
                            this.OutputABC ^= 4;
                        }
                        if (++this._CounterBend >= this.FreqBend)
                        {
                            this._CounterBend = 0;
                            this.ChangeEnvelope();
                        }
                    }
                }
            }
        }

        private void env_DD()
        {
            this.BendStatus = 0;
            this.BendVolumeIndex = 0;
        }

        private void env_DU()
        {
            this.BendStatus = 1;
            this.BendVolumeIndex = 0;
        }

        private void env_UD()
        {
            this.BendStatus = 2;
            this.BendVolumeIndex = MAX_ENV_VOLTBL;
        }

        private void env_UU()
        {
            this.BendStatus = 0;
            this.BendVolumeIndex = MAX_ENV_VOLTBL;
        }

        #endregion
    }
}